﻿namespace ElisVK.Controls
{
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;

    [TemplatePart(Name = "PART_ScrollViewer", Type = typeof(ScrollViewer))]
    public class ZoomScrollViewer : Control
    {
        #region Fields

        public static readonly RoutedCommand ZoomInCommand = new RoutedCommand("ZoomIn", typeof(ZoomScrollViewer));
        public static readonly RoutedCommand ZoomOutCommand = new RoutedCommand("ZoomOut", typeof(ZoomScrollViewer));
        public static readonly RoutedCommand ZoomAutoCommand = new RoutedCommand("ZoomAuto", typeof(ZoomScrollViewer));

        private static readonly DependencyProperty ContentProperty;
        private static readonly DependencyProperty ZoomProperty;
        private static readonly DependencyProperty ZoomMaxProperty;
        private static readonly DependencyProperty ZoomStepProperty;

        private ScrollViewer _scrollViewer;
        private Point _pointMouseDown;

        #endregion

        #region Implementation

        static ZoomScrollViewer()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(ZoomScrollViewer), new FrameworkPropertyMetadata(typeof(ZoomScrollViewer)));

            ContentProperty = DependencyProperty.Register(
                "Content", typeof(FrameworkElement), typeof(ZoomScrollViewer), new PropertyMetadata(OnContentPropertyChanged));

            ZoomProperty = DependencyProperty.Register(
                "Zoom", typeof(double), typeof(ZoomScrollViewer), new PropertyMetadata(1.0, OnZoomPropertyChanged));

            ZoomMaxProperty = DependencyProperty.Register(
                "ZoomMax", typeof(double), typeof(ZoomScrollViewer), new PropertyMetadata(5.0, OnZoomMaxPropertyChanged));

            ZoomStepProperty = DependencyProperty.Register(
                "ZoomStep", typeof(double), typeof(ZoomScrollViewer), new PropertyMetadata(0.2, OnZoomStepPropertyChanged));

            CommandManager.RegisterClassCommandBinding(
                typeof(ZoomScrollViewer), new CommandBinding(ZoomInCommand, OnZoomInCommand, CanExecuteZoomInCommand));

            CommandManager.RegisterClassCommandBinding(
                typeof(ZoomScrollViewer), new CommandBinding(ZoomOutCommand, OnZoomOutCommand, CanExecuteZoomOutCommand));

            CommandManager.RegisterClassCommandBinding(
                typeof(ZoomScrollViewer), new CommandBinding(ZoomAutoCommand, OnZoomAutoCommand, CanExecuteZoomAutoCommand));
        }

        #region Public properties

        public FrameworkElement Content
        {
            get
            {
                return (FrameworkElement)GetValue(ContentProperty);
            }

            set
            {
                SetValue(ContentProperty, value);
            }
        }

        public double Zoom
        {
            get
            {
                return (double)GetValue(ZoomProperty);
            }

            set
            {
                SetValue(ZoomProperty, value);
            }
        }

        public double ZoomMax
        {
            get
            {
                return (double)GetValue(ZoomMaxProperty);
            }

            set
            {
                SetValue(ZoomMaxProperty, value);
            }
        }

        public double ZoomStep
        {
            get
            {
                return (double)GetValue(ZoomStepProperty);
            }

            set
            {
                SetValue(ZoomStepProperty, value);
            }
        }

        #endregion

        protected FrameworkElement ScrollViewerContent
        {
            get
            {
                FrameworkElement fweContent = null;

                if (_scrollViewer != null)
                    fweContent = _scrollViewer.Content as FrameworkElement;

                return fweContent;
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (_scrollViewer != null)
            {
                _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
                _scrollViewer.PreviewMouseWheel -= ScrollViewer_PreviewMouseWheel;
            }

            _scrollViewer = GetTemplateChild("PART_ScrollViewer") as ScrollViewer;

            if (_scrollViewer != null)
            {
                _scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
                _scrollViewer.PreviewMouseWheel += ScrollViewer_PreviewMouseWheel;

                SetContent();
            }
        }

        protected virtual void OnContentChanged(DependencyPropertyChangedEventArgs e)
        {
            SetContent();

            FrameworkElement fweContent = ScrollViewerContent;

            if (fweContent != null)
                fweContent.Cursor = Zoom > 1.0 ? Cursors.ScrollAll : Cursors.Arrow;
        }

        protected virtual void OnZoomChanged(DependencyPropertyChangedEventArgs e)
        {
            SetContentZoom();

            CommandManager.InvalidateRequerySuggested();

            FrameworkElement fweContent = ScrollViewerContent;

            if (fweContent != null)
                fweContent.Cursor = Zoom > 1.0 ? Cursors.ScrollAll : Cursors.Arrow;
        }

        protected virtual void OnZoomMaxChanged(DependencyPropertyChangedEventArgs e)
        {
            if (Zoom > ZoomMax)
                Zoom = ZoomMax;
        }

        protected virtual void OnZoomStepChanged(DependencyPropertyChangedEventArgs e)
        {
        }

        protected virtual void OnZoomIn()
        {
            Zoom = Math.Min(Zoom + ZoomStep, ZoomMax);
        }

        protected virtual void CanExecuteZoomIn(CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = Zoom < ZoomMax;
        }

        protected virtual void OnZoomOut()
        {
            Zoom = Math.Max(Zoom - ZoomStep, 1.0);
        }

        protected virtual void CanExecuteZoomOut(CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = Zoom > 1.0;
        }

        protected virtual void OnZoomAuto()
        {
            Zoom = 1.0;
        }

        protected virtual void CanExecuteZoomAuto(CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = Zoom != 1.0;
        }

        protected void SetContent()
        {
            if (_scrollViewer != null)
            {
                _scrollViewer.Content = null;

                FrameworkElement content = Content;

                if (content != null)
                {
                    content.MouseDown += Content_MouseDown;
                    content.MouseMove += Content_MouseMove;
                    content.MouseUp += Content_MouseUp;

                    _scrollViewer.Content = content;

                    ResetZoomAndPosition();

                    SetContentZoom();
                }
            }
        }

        protected void ProcessPanningMouse(MouseEventArgs e, bool isReleaseCapture)
        {
            FrameworkElement fweContent = ScrollViewerContent;

            if (fweContent != null && fweContent.IsMouseCaptured)
            {
                Point pointMouse = e.GetPosition(fweContent);
                Vector deltaMouse = pointMouse - _pointMouseDown;

                _scrollViewer.ScrollToHorizontalOffset(_scrollViewer.HorizontalOffset - deltaMouse.X);
                _scrollViewer.ScrollToVerticalOffset(_scrollViewer.VerticalOffset - deltaMouse.Y);

                if (isReleaseCapture)
                    fweContent.ReleaseMouseCapture();
            }
        }

        protected void SetContentZoom()
        {
            FrameworkElement content = Content;

            if (_scrollViewer != null && content != null)
            {
                double zoom = Zoom;

                double width = zoom * _scrollViewer.ViewportWidth;
                double height = zoom * _scrollViewer.ViewportHeight;

                content.Width = width;
                content.Height = height;
            }
        }

        private static void OnContentPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var zsv = d as ZoomScrollViewer;

            if (zsv != null)
                zsv.OnContentChanged(e);
        }

        private static void OnZoomPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var zsv = d as ZoomScrollViewer;

            if (zsv != null)
                zsv.OnZoomChanged(e);
        }

        private static void OnZoomMaxPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var zsv = d as ZoomScrollViewer;

            if (zsv != null)
                zsv.OnZoomMaxChanged(e);
        }

        private static void OnZoomStepPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var zsv = d as ZoomScrollViewer;

            if (zsv != null)
                zsv.OnZoomStepChanged(e);
        }

        private static void OnZoomInCommand(object target, ExecutedRoutedEventArgs e)
        {
            var zsv = target as ZoomScrollViewer;

            if (zsv != null)
                zsv.OnZoomIn();
        }

        private static void CanExecuteZoomInCommand(object sender, CanExecuteRoutedEventArgs e)
        {
            var zsv = sender as ZoomScrollViewer;

            if (zsv != null)
                zsv.CanExecuteZoomIn(e);
        }

        private static void OnZoomOutCommand(object target, ExecutedRoutedEventArgs e)
        {
            var zsv = target as ZoomScrollViewer;

            if (zsv != null)
                zsv.OnZoomOut();
        }

        private static void CanExecuteZoomOutCommand(object sender, CanExecuteRoutedEventArgs e)
        {
            var zsv = sender as ZoomScrollViewer;

            if (zsv != null)
                zsv.CanExecuteZoomOut(e);
        }

        private static void OnZoomAutoCommand(object target, ExecutedRoutedEventArgs e)
        {
            var zsv = target as ZoomScrollViewer;

            if (zsv != null)
                zsv.OnZoomAuto();
        }

        private static void CanExecuteZoomAutoCommand(object sender, CanExecuteRoutedEventArgs e)
        {
            var zsv = sender as ZoomScrollViewer;

            if (zsv != null)
                zsv.CanExecuteZoomAuto(e);
        }

        private void ResetZoomAndPosition()
        {
            Zoom = 1;
            _scrollViewer.ScrollToHome();
            _scrollViewer.ScrollToLeftEnd();
        }

        private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            if (e.ViewportWidthChange != 0 || e.ViewportHeightChange != 0)
                SetContentZoom();
        }

        private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta > 0)
            {
                for (int i = 0; i < Math.Abs(e.Delta / 120); i++)
                    OnZoomIn();
            }
            else if (e.Delta < 0)
            {
                for (int i = 0; i < Math.Abs(e.Delta / 120); i++)
                    OnZoomOut();
            }

            e.Handled = true;
        }

        private void Content_MouseDown(object sender, MouseButtonEventArgs e)
        {
            FrameworkElement fweContent = ScrollViewerContent;

            if (fweContent != null)
            {
                _pointMouseDown = e.GetPosition(fweContent);

                fweContent.CaptureMouse();
            }
        }

        private void Content_MouseMove(object sender, MouseEventArgs e)
        {
            ProcessPanningMouse(e, false);
        }

        private void Content_MouseUp(object sender, MouseButtonEventArgs e)
        {
            ProcessPanningMouse(e, true);
        }

        #endregion
    }
}
